﻿#region [===== Using =====]
using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Workflow;
using System.ComponentModel;
using System.Globalization;
using System.Security.Permissions;
#endregion

namespace TPG.SharePoint.Workflow
{
    /// <summary>
    /// A base page for workflow association forms.
    /// </summary>
    /// <remarks>
    /// <para>This class implements the generic framework code for handling workflow
    /// associations. The following tasks are performed by this class.</para>
    /// <list type="bullet">
    ///     <item>Creating task and history lists</item>
    ///     <item>Creating and updating workflow association objects</item>
    /// </list>
    /// <para>An <see cref="AssociationPage" /> is an ASPX page displayed 
    /// during workflow association. As the implementer of workflow association 
    /// forms you can derive from this class to implement the generic aspects of the
    /// workflow form. The non-generic data contained in the form is available as a
    /// <see cref="ConfigurationData" /> object. Using the
    /// <see cref="AssociationPage.CommitAssociation" /> method you commit the 
    /// association and provide custom data for the workflow. The 
    /// <see cref="AssociationPage.CancelAssociation" /> method redirects the 
    /// user back to the workflow settings page without storing any data and 
    /// without performing the association. Existing association data can be 
    /// retrieved during updates of a workflow association using the 
    /// <see cref="AssociationPage.GetAssociationData" /> method.</para>
    /// </remarks>
    public abstract class AssociationPage
        : LayoutWorkflowPage
    {
        #region [===== Inner classes =====]
        class OnOffConverter : BooleanConverter
        {
            public override object ConvertFrom(
                ITypeDescriptorContext context,
                CultureInfo culture,
                object value)
            {
                string valueString = value as string;
                if (String.IsNullOrEmpty(valueString))
                {
                    return false;
                }
                if (String.Equals(valueString, "on",
                    StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
                if (String.Equals(valueString, "off",
                    StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }
                return base.ConvertFrom(context, culture, value);
            }
        }
        #endregion

        #region [===== Instance fields =====]
        bool? _isInitialLoad;
        SPContentType _contentType;
        SPContentType _listContentType;
        #endregion

        #region [===== Properties =====]

        #region [===== Public state =====]
        /// <summary>
        /// Gets a value indicating whether the form is opened
        /// for the first time.
        /// </summary>
        public sealed override bool IsInitialLoad
        {
            get
            {
                return _isInitialLoad ?? true;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this association form is used
        /// to create a new association or edit an existing one.
        /// </summary>
        public bool IsNewAssociation
        {
            get { return ExistingAssociationID == null; }
        }

        /// <summary>
        /// Gets the permissions required to view the page.
        /// </summary>
        /// <remarks>
        /// This method returns a different value depending on the association
        /// type. 
        /// <list type="table">
        ///     <item>
        ///         <term>List</term>
        ///         <description>SPBasePermissions.ManageLists</description>
        ///     </item>
        ///     <item>
        ///         <term>ListContentType</term>
        ///         <description>SPBasePermissions.ManageLists</description>
        ///     </item>
        ///     <item>
        ///         <term>ContentType</term>
        ///         <description>SPBasePermissions.AddAndCustomizePages</description>
        ///     </item>
        /// </list>
        /// </remarks>
        protected override SPBasePermissions RightsRequired
        {
            get
            {
                SPBasePermissions rightsRequired;
                if (WorkflowAssociationType == AssociationType.List ||
                    WorkflowAssociationType == AssociationType.ListContentType)
                {
                    rightsRequired = (SPBasePermissions.EmptyMask | SPBasePermissions.ManageLists);
                }
                else
                {
                    rightsRequired = (SPBasePermissions.EmptyMask | SPBasePermissions.AddAndCustomizePages);
                }
                return rightsRequired;
            }
        }

        #endregion

        #region [===== Query-string Parameters =====]
        /// <summary>
        /// Gets the name of the workflow as specified on the first form
        /// displayed to the end-user during workflow association.
        /// </summary>
        protected string WorkflowName
        {
            get { return QueryStringParser.GetParameter<String>("WorkflowName"); }
        }

        /// <summary>
        /// Gets the <see cref="AssociationType" /> indicating with what
        /// kind of target the association is being performed.
        /// </summary>
        protected AssociationType WorkflowAssociationType
        {
            get
            {
                Guid? listID = ListID;
                string contentTypeID = ContentTypeID;
                AssociationType type = AssociationType.List;
                if (String.IsNullOrEmpty(contentTypeID) == false &&
                    listID != null)
                {
                    type = AssociationType.ListContentType;
                }
                else if (String.IsNullOrEmpty(contentTypeID) &&
                    listID != null)
                {
                    type = AssociationType.List;
                }
                else if (String.IsNullOrEmpty(contentTypeID) == false &&
                    listID == null)
                {
                    type = AssociationType.ContentType;
                }
                else
                {
                    throw new SPException("Association type could not be determined");
                }
                return type;
            }
        }

        /// <summary>
        /// Gets the name of the target with which the association is 
        /// performed.
        /// </summary>
        /// <remarks>
        /// For list associations this method returns the list title, otherwise
        /// the content-type name is returned.
        /// </remarks>
        protected string AssociationTargetName
        {
            get
            {
                return WorkflowAssociationType == AssociationType.List ? List.Title :
                    WorkflowAssociationType == AssociationType.ContentType ? SiteContentType.Name :
                    ListContentType.Name;
            }
        }

        Guid WorkflowTemplateID
        {
            get { return QueryStringParser.GetParameter<Guid>("WorkflowDefinition"); }
        }

        Guid? ExistingAssociationID
        {
            get { return QueryStringParser.GetParameterAllowNull<Guid?>("GuidAssoc"); }
        }

        Guid? ListID
        {
            get { return QueryStringParser.GetParameterAllowNull<Guid?>("List"); }
        }

        string ContentTypeID
        {
            get { return QueryStringParser.GetParameterAllowNull<String>("ctype"); }
        }

        bool AutoStartWorkflowOnCreate
        {
            get
            {
                return QueryStringParser.GetParameter<bool>(
                    "AutoStartCreate", true, new OnOffConverter());
            }
        }

        bool AutoStartWorkflowOnChange
        {
            get
            {
                return QueryStringParser.GetParameter<bool>(
                    "AutoStartChange", true, new OnOffConverter());
            }
        }

        bool ManualStartWorkflow
        {
            get
            {
                return QueryStringParser.GetParameter<bool>(
                    "AllowManual", true, new OnOffConverter());
            }
        }

        string TaskListID
        {
            get { return QueryStringParser.GetParameter<String>("TaskList"); }
        }

        string HistoryListID
        {
            get { return QueryStringParser.GetParameter<String>("HistoryList"); }
        }

        bool CascadeContentTypeToList
        {
            get { return QueryStringParser.GetParameter<bool>("UpdateLists"); }
        }

        bool ManualPermManageListRequired
        {
            get { return QueryStringParser.GetParameter<bool>("ManualPermManageListRequired", true, new OnOffConverter()); }
        }

        bool SetDefault
        {
            get { return QueryStringParser.GetParameter<bool>("SetDefault", true, new OnOffConverter()); }
        }

        bool AllowEdits
        {
            get { return QueryStringParser.GetParameter<bool?>("AllowEdits", true) ?? true; }
        }        
        #endregion

        #region [===== Workflow association target cache =====]
        /// <summary>
        /// Gets the site-level <see cref="SPContentType" /> association target.
        /// </summary>
        /// <remarks>
        /// This property returns null when the <see cref="AssociationType" /> is 
        /// not set to <see cref="AssociationType.ContentType" />
        /// </remarks>
        protected SPContentType SiteContentType
        {
            get
            {
                if (_contentType == null && WorkflowAssociationType == AssociationType.ContentType)
                {
                    SPContentTypeId contentTypeID = new SPContentTypeId(ContentTypeID);
                    _contentType = Web.AvailableContentTypes[contentTypeID];
                }
                return _contentType;
            }
        }

        /// <summary>
        /// Gets the list-level <see cref="SPContentType" /> association target.
        /// </summary>
        /// <remarks>
        /// This property returns null when the <see cref="AssociationType" /> is 
        /// not set to <see cref="AssociationType.ListContentType" />
        /// </remarks>
        protected SPContentType ListContentType
        {
            get
            {
                if (_listContentType == null && WorkflowAssociationType == AssociationType.ListContentType)
                {
                    SPList list = Web.Lists[ListID.Value];
                    SPContentTypeId contentTypeID = new SPContentTypeId(ContentTypeID);
                    _listContentType = list.ContentTypes[contentTypeID];
                }
                return _listContentType;
            }
        }
        /// <summary>
        /// Gets the list association target.
        /// </summary>
        /// <remarks>
        /// This property returns null when the <see cref="AssociationType" /> is 
        /// not set to <see cref="AssociationType.List" />
        /// </remarks>
        protected SPList List
        {
            get { return SPContext.Current.List; }
        }
        #endregion

        #endregion

        #region [===== Constructor =====]
        /// <summary>
        /// Initializes a new instance of the <see cref="AssociationPage" /> 
        /// class.
        /// </summary>
        protected AssociationPage()
        {
        }
        #endregion

        #region [===== Public methods =====]
        /// <summary>
        /// Supresses validation on the first load of the page.
        /// </summary>
        /// <remarks>Since the Association page is first visited as a postback, 
        /// validation fires immediatly on page load.  To supress this initial
        /// validation, the base class's implementation of validate is only 
        /// called on postbacks after the initial load.</remarks>
        public override void Validate()
        {
            if (!IsInitialLoad)
                base.Validate();
        }

        /// <summary>
        /// Supresses validation on the first load of the page.
        /// </summary>
        /// <remarks>Since the Association page is first visited as a postback, 
        /// validation fires immediatly on page load.  To supress this initial 
        /// validation, the base class's implementation of validate is only 
        /// called on postbacks after the initial load.</remarks>
        public override void Validate(string validationGroup)
        {
            if (!IsInitialLoad)
                base.Validate(validationGroup);
        }
        #endregion

        #region [===== Internal methods =====]
        internal override SPWorkflowTemplate GetWorkflowTemplate()
        {
            return Web.WorkflowTemplates[WorkflowTemplateID];
        }

        internal override SPWorkflowAssociation GetWorkflowAssociation()
        {
            SPWorkflowAssociation association = null;
            if (ExistingAssociationID != null)
            {
                association = GetExistingAssociation();
            }
            return association;
        }
        internal override WorkflowMetaData GetMetaData()
        {
            return new AssociationMetaData()
            {
                AssociationTargetName = AssociationTargetName,
                AssociationType = WorkflowAssociationType,
                IsNewAssociation = IsNewAssociation,
                PageType = PageType.Association,
                WorkflowName = WorkflowName,
                SiteContentType = SiteContentType, 
                ListContentType = ListContentType,
                List = List
            };
        }
        #endregion

        #region [===== Protected methods =====]

        #region [===== Page lifecycle =====]
        /// <summary>
        /// Registers the page as a control state container.
        /// </summary>
        /// <param name="e">An EventArgs containing event data.</param>
        [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
        protected override void OnInit(EventArgs e)
        {
            RegisterRequiresControlState(this);
            base.OnInit(e);
        }

        /// <summary>
        /// Loads the appropriate state for the page.
        /// </summary>
        /// <param name="savedState">The saved data.</param>
        protected override void LoadControlState(object savedState)
        {
            _isInitialLoad = savedState == null;
        }

        /// <summary>
        /// Saves the appropriate state for the page.
        /// </summary>
        /// <returns>The saved data.</returns>
        protected override object SaveControlState()
        {
            bool isInitialLoad = false;
            return isInitialLoad;
        }

        /// <summary>
        /// Passes through workflow parameters as hidden fields.
        /// </summary>
        /// <param name="e">An <see cref="EventArgs"/> object
        /// representing event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            string[] passthroughParameters = 
            { 
                    "WorkflowName", "AddToStatusMenu", "AllowManual",
                    "RoleSelect", "AutoStartCreate", "AutoStartChange",
                    "GuidAssoc", "SetDefault", "HistoryList",
                    "TaskList", "UpdateLists", "WorkflowDefinition",
                    "ManualPermManageListRequired", "AllowEdits"
            };
            foreach (string parameter in passthroughParameters)
            {
                ClientScript.RegisterHiddenField(
                    parameter,
                    QueryStringParser.GetParameterAllowNull<String>(parameter));
            }
            base.OnPreRender(e);
        }
        #endregion

        #region [===== Association =====]
        /// <summary>
        /// Commits the association with the specified data and redirects to 
        /// the workflow settings page.
        /// </summary>
        /// <remarks>
        /// This method should be called for committing both new and existing
        /// associations. Custom association data can be passed in as a 
        /// <see cref="ConfigurationData" /> object.
        /// </remarks>
        /// <param name="data">The data to pass to the workflow as 
        /// association data.</param>
        /// <exception cref="ArgumentNullException">Thrown when the 
        /// <paramref name="data"/> is not specified.</exception>
        protected void CommitAssociation(ConfigurationData data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            using (SPLongOperation operation = new SPLongOperation(this))
            {
                operation.LeadingHTML = GetProgressHtml();
                operation.Begin();

                SPList taskList = null;
                SPList historyList = null;
                if (WorkflowAssociationType != AssociationType.ContentType)
                {
                    // Make sure the Tasks list exists
                    // The id or name of the list is in the 
                    // query string parameters
                    taskList = EnsureList(
                        TaskListID,
                        SPListTemplateType.Tasks);
                    // Make sure the History list exists
                    // The id or name of the list is in the 
                    // query string parameters
                    historyList = EnsureList(
                        HistoryListID,
                        SPListTemplateType.WorkflowHistory);
                }

                SPWorkflowAssociation association;
                if (IsNewAssociation)
                {
                    association = CreateAssociation(data.NativeData, taskList, historyList);
                }
                else
                {
                    association = WorkflowAssociationInternal;
                    UpdateAssocation(association, data.NativeData, taskList, historyList);
                }
                if (WorkflowAssociationType != AssociationType.ContentType)
                {
                    if (List.EnableMinorVersions)
                    {
                        if (this.SetDefault)
                        {
                            if (!List.EnableModeration)
                            {
                                List.EnableModeration = true;
                                List.DraftVersionVisibility = DraftVisibilityType.Approver;
                            }
                            List.DefaultContentApprovalWorkflowId = association.Id;
                            List.Update();
                        }
                        else if (List.DefaultContentApprovalWorkflowId == association.Id)
                        {
                            List.DefaultContentApprovalWorkflowId = Guid.Empty;
                            List.Update();
                        }
                    }
                }
                operation.End(BuildRedirectString("WrkSetng.aspx"),
                    SPRedirectFlags.RelativeToLayoutsPage, this.Context, null);
            }
        }

        /// <summary>
        /// Cancels the association by redirecting to 
        /// the workflow settings page.
        /// </summary>
        protected void CancelAssociation()
        {
            string redirectString = BuildRedirectString("AddWrkfl.aspx");
            if (!IsNewAssociation)
                redirectString += string.Format(CultureInfo.InvariantCulture, "&TemplateID={0}",
                    SPHttpUtility.UrlKeyValueEncode(ExistingAssociationID.Value));

            SPUtility.Redirect(redirectString,
                SPRedirectFlags.RelativeToLayoutsPage, Context);
        }

        /// <summary>
        /// Gets the data from an existing workflow association.
        /// </summary>
        /// <returns>The data encapsulated in a 
        /// <see cref="ConfigurationData" /> object.</returns>
        protected ConfigurationData GetAssociationData()
        {
            string data = null;
            if (IsNewAssociation == false)
            {
                SPWorkflowAssociation association = WorkflowAssociationInternal;
                data = association.AssociationData;
            }
            return ConfigurationData.CreateUsingStringData(data);
        }

        /// <summary>
        /// Returns a metadata property from the workflow template.
        /// </summary>
        /// <param name="name">The name of the property to retrieve.</param>
        /// <returns>The value of the property.</returns>
        protected object GetWorkflowTemplateData(string name)
        {
            return WorkflowTemplateInternal[name];
        }

        /// <summary>
        /// Gets the HTML to display in the progress screen.
        /// </summary>
        /// <returns>A string of HTML, without the HTML, 
        /// BODY or FORM tags</returns>
        protected virtual string GetProgressHtml()
        {
            return null;
        }
        #endregion

        #endregion

        #region [===== Private methods =====]
        SPWorkflowAssociation GetExistingAssociation()
        {
            SPWorkflowAssociation association = null;
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    association = List.WorkflowAssociations[ExistingAssociationID.Value];
                    break;
                case AssociationType.ContentType:
                    association = SiteContentType.WorkflowAssociations[ExistingAssociationID.Value];
                    break;
                case AssociationType.ListContentType:
                    association = ListContentType.WorkflowAssociations[ExistingAssociationID.Value];
                    break;
            }
            return association;
        }

        SPWorkflowAssociation CreateAssociation(string data, SPList taskList, SPList historyList)
        {
            SPWorkflowTemplate workflowTemplate = WorkflowTemplateInternal;
            SPWorkflowAssociation association = null;
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    association = SPWorkflowAssociation.CreateListAssociation(
                        workflowTemplate, WorkflowName, taskList, historyList);
                    UpdateAssociationProperties(association, data, taskList, historyList);
                    List.AddWorkflowAssociation(association);
                    break;
                case AssociationType.ContentType:
                    association = SPWorkflowAssociation.CreateSiteContentTypeAssociation(
                        workflowTemplate, WorkflowName, this.TaskListID, this.HistoryListID);
                    UpdateAssociationProperties(association, data, taskList, historyList);
                    SiteContentType.AddWorkflowAssociation(association);
                    if (CascadeContentTypeToList)
                    {
                        SiteContentType.UpdateWorkflowAssociationsOnChildren(true, true, true);
                    }
                    break;
                case AssociationType.ListContentType:
                    association = SPWorkflowAssociation.CreateListContentTypeAssociation(
                        workflowTemplate, WorkflowName, taskList, historyList);
                    UpdateAssociationProperties(association, data, taskList, historyList);
                    ListContentType.AddWorkflowAssociation(association);
                    break;
            }

            return association;
        }

        void UpdateAssocation(SPWorkflowAssociation assocation,
            string data, SPList taskList, SPList historyList)
        {
            UpdateAssociationProperties(assocation, data, taskList, historyList);
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    List.UpdateWorkflowAssociation(assocation);
                    break;
                case AssociationType.ContentType:
                    SiteContentType.UpdateWorkflowAssociation(assocation);
                    if (CascadeContentTypeToList)
                    {
                        SiteContentType.UpdateWorkflowAssociationsOnChildren(true, true, true);
                    }
                    break;
                case AssociationType.ListContentType:
                    ListContentType.UpdateWorkflowAssociation(assocation);
                    break;
            }
        }

        void UpdateAssociationProperties(
            SPWorkflowAssociation association, string data,
            SPList taskList, SPList historyList)
        {
            association.Name = WorkflowName;
            association.LockItem = !AllowEdits;
            association.AutoStartCreate = AutoStartWorkflowOnCreate;
            association.AutoStartChange = AutoStartWorkflowOnChange;
            association.AllowManual = ManualStartWorkflow;
            if (association.AllowManual)
                association.PermissionsManual = ManualPermManageListRequired ?
                    SPBasePermissions.EmptyMask | SPBasePermissions.ManageLists : SPBasePermissions.EmptyMask;


            if (WorkflowAssociationType != AssociationType.ContentType)
            {
                if (association.TaskListId != taskList.ID)
                {
                    association.SetTaskList(taskList);
                }
                if (association.HistoryListId != historyList.ID)
                {
                    association.SetHistoryList(historyList);
                }
            }
            else
            {
                if (association.TaskListTitle != this.TaskListID)
                {
                    association.TaskListTitle = this.TaskListID;
                }
                if (association.HistoryListTitle != this.HistoryListID)
                {
                    association.HistoryListTitle = this.HistoryListID;
                }
            }

            association.AssociationData = data;            
        }

        SPList EnsureList(string listIdentifier,
            SPListTemplateType listTemplateType)
        {
            SPList list = null;
            if (listIdentifier[0] != 'z')
            {
                list = GetListByID(listIdentifier);
            }
            else
            {
                list = EnsureNewNamedList(listIdentifier.Substring(1), listTemplateType);
            }
            return list;
        }

        SPList EnsureNewNamedList(string listIdentifier,
            SPListTemplateType listTemplateType)
        {
            SPList list = null;

            if (TryGetList(listIdentifier, out list) == true)
            {
                int maxRange = 1000;
                bool listCreated = false;
                int currentPostfix = 0;
                while (listCreated == false && currentPostfix < maxRange)
                {
                    string newListIdentifier = listIdentifier + currentPostfix++;
                    SPList existingList = null;
                    if (TryGetList(newListIdentifier, out existingList) == false)
                    {
                        list = CreateList(
                            newListIdentifier, listTemplateType);
                        break;
                    }
                }
                if (list == null)
                {
                    throw new WorkflowException(
                        String.Format(
                        "{0} list could not be created", listTemplateType));
                }
            }
            else
            {
                list = CreateList(
                    listIdentifier, listTemplateType);
            }
            return list;
        }

        SPList GetListByID(string listIdentifier)
        {
            Guid listID = new Guid(listIdentifier);
            return Web.Lists[listID];
        }

        SPList CreateList(string listIdentifier, SPListTemplateType listTemplateType)
        {
            string description = String.Format(CultureInfo.CurrentCulture,
                "{0} list for the {1} workflow.", listTemplateType, WorkflowName);
            Guid listID = Web.Lists.Add(
                listIdentifier, description,
                listTemplateType);
            return Web.Lists[listID];
        }

        bool TryGetList(string listIdentifier, out SPList list)
        {
            try
            {
                list = Web.Lists[listIdentifier];
                return true;
            }
            catch (ArgumentException)
            {
                list = null;
                return false;
            }
        }

        string BuildRedirectString(string pageUrl)
        {
            string redirect = null;
            switch (WorkflowAssociationType)
            {
                case AssociationType.List:
                    redirect = pageUrl + "?List=" + QueryStringParser.GetParameter<String>("List");
                    break;
                case AssociationType.ContentType:
                    redirect = pageUrl + "?ctype=" + QueryStringParser.GetParameter<String>("ctype");
                    break;
                case AssociationType.ListContentType:
                    redirect = pageUrl + "?List=" + QueryStringParser.GetParameter<String>("List") +
                        "&ctype=" + QueryStringParser.GetParameter<String>("ctype");
                    break;
            }
            return redirect;
        }
        #endregion
    }
}
